10 设计模式——命令模式

返回设计模式博客目录

介绍


命令(Command)模式:将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作

在软件开发系统中,常常出现“方法的请求者”与“方法的实现者”之间存在紧密的耦合关系。这不利于软件功能的扩展与维护。例如,想对行为进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与方法的实现者解耦?”变得很重要,命令模式能很好地解决这个问题。

在现实生活中,这样的例子也很多,例如,电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者),还有计算机键盘上的“功能键”等。

使用场景

对于大多数请求——响应模式的功能,比较适合使用命令模式。

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队(如:线程池+工作队列)和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作(比如系统挂掉之后重启做一些恢复操作,还有数据库的事务等)。
  • 系统需要将一组操作组合在一起,即支持宏命令。

优点

  1. 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  2. 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  3. 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  4. 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

缺点

可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。

结构与实现


命令模式包含以下主要角色。

  • 抽象命令类(Command):声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  • 具体命令角色(Concrete Command):是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver):执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  • 调用者/请求者(Invoker):是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

其结构图如下图所示。

命令模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class CommandPattern {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command cmd = new ConcreteCommand(receiver);
Invoker invoker = new Invoker(cmd);
invoker.call();
}
}
// 调用者
class Invoker {
private Command command;
public Invoker(Command command) {
this.command=command;
}
public void setCommand(Command command) {
this.command=command;
}
public void call() {
// 调用具体命令对象的相关方法,执行具体命令
command.execute();
}
}
// 抽象命令
interface Command {
void execute();
}
// 具体命令
class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
public void execute() {
receiver.action();
}
}
// 接收者
class Receiver {
// 真正执行具体命令逻辑的方法
public void action() {
System.out.println("接收者执行具体操作");
}
}

示例


用命令模式实现客户去餐馆吃早餐的实例。

分析:客户去餐馆可选择的早餐有肠粉、河粉和馄饨等,客户可向服务员选择以上早餐中的若干种,服务员将客户的请求交给相关的厨师去做。这里的点早餐相当于“命令”,服务员相当于“调用者”,厨师相当于“接收者”,所以用命令模式实现比较合适。

首先,定义一个早餐类(Breakfast),它是抽象命令类,有抽象方法 cooking(),说明要做什么;再定义其子类肠粉类(ChangFen)、馄饨类(HunTun)和河粉类(HeFen),它们是具体命令类,实现早餐类的 cooking() 方法,但它们不会具体做,而是交给具体的厨师去做;具体厨师类有肠粉厨师(ChangFenChef)、馄蚀厨师(HunTunChef)和河粉厨师(HeFenChef),他们是命令的接收者,所以把每个厨师类定义为 JFrame 的子类;最后,定义服务员类(Waiter),它接收客户的做菜请求,并发出做菜的命令。客户类是通过服务员类来点菜的,下图所示是其结构图。

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class CookingCommand {
public static void main(String[] args) {
Breakfast food1 = new ChangFen();
Breakfast food2 = new HunTun();
Breakfast food3 = new HeFen();
Waiter waiter = new Waiter();
waiter.setChangFen(food1);//设置肠粉菜单
waiter.setHunTun(food2); //设置河粉菜单
waiter.setHeFen(food3); //设置馄饨菜单
waiter.chooseChangFen(); //选择肠粉
waiter.chooseHeFen(); //选择河粉
waiter.chooseHunTun(); //选择馄饨
}
}
// 调用者:服务员
class Waiter {
private Breakfast changFen, hunTun, heFen;
public void setChangFen(Breakfast f) {
changFen = f;
}
public void setHunTun(Breakfast f) {
hunTun=f;
}
public void setHeFen(Breakfast f) {
heFen=f;
}
public void chooseChangFen() {
changFen.cooking();
}
public void chooseHunTun() {
hunTun.cooking();
}
public void chooseHeFen() {
heFen.cooking();
}
}
// 抽象命令:早餐
interface Breakfast {
void cooking();
}
// 具体命令:肠粉
class ChangFen implements Breakfast {
private ChangFenChef receiver;
ChangFen() {
receiver = new ChangFenChef();
}
public void cooking() {
receiver.cooking();
}
}
// 具体命令:馄饨
class HunTun implements Breakfast {
private HunTunChef receiver;
HunTun() {
receiver=new HunTunChef();
}
public void cooking() {
receiver.cooking();
}
}
// 具体命令:河粉
class HeFen implements Breakfast {
private HeFenChef receiver;
HeFen() {
receiver=new HeFenChef();
}
public void cooking() {
receiver.cooking();
}
}
// 接收者:肠粉厨师
class ChangFenChef extends JFrame {
private static final long serialVersionUID = 1L;
JLabel l = new JLabel();
ChangFenChef() {
super("煮肠粉");
l.setIcon(new ImageIcon("src/command/ChangFen.jpg"));
this.add(l);
this.setLocation(30, 30);
this.pack();
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void cooking() {
this.setVisible(true);
}
}
// 接收者:馄饨厨师
class HunTunChef extends JFrame {
private static final long serialVersionUID=1L;
JLabel l = new JLabel();
HunTunChef() {
super("煮馄饨");
l.setIcon(new ImageIcon("src/command/HunTun.jpg"));
this.add(l);
this.setLocation(350, 50);
this.pack();
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void cooking() {
this.setVisible(true);
}
}
// 接收者:河粉厨师
class HeFenChef extends JFrame {
private static final long serialVersionUID=1L;
JLabel l = new JLabel();
HeFenChef() {
super("煮河粉");
l.setIcon(new ImageIcon("src/command/HeFen.jpg"));
this.add(l);
this.setLocation(200, 280);
this.pack();
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void cooking() {
this.setVisible(true);
}
}

实战


命令模式在 GUI 上应用广泛,比如手写签名功能,需要提供撤销或重做等功能。

  • 首先声明一个抽象接口 IBrush,用它定义不同笔触需要实现的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IBrush {
/**
* 触点接触时
* @param path 路径对象
* @param x 当前位置的 x 坐标
* @param y 当前位置的 y 坐标
*/
void down(Path path, float x, float y);
/**
* 触点移动时
* @param path 路径对象
* @param x 当前位置的 x 坐标
* @param y 当前位置的 y 坐标
*/
void move(Path path, float x, float y);
/**
* 触点离开时
* @param path 路径对象
* @param x 当前位置的 x 坐标
* @param y 当前位置的 y 坐标
*/
void up(Path path, float x, float y);
}
  • 为了简便起见,这里只定义两种类型的笔触,一种为普通的线条,另一种为由圆点组成的线条轨迹。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 普通笔触
public class NormalBrush implements IBrush {
@Override
public void down(Path path, float x, float y) {
path.moveTo(x, y);
}
@Override
public void move(Path path, float x, float y) {
path.lineTo(x, y);
}
@Override
public void up(Path path, float x, float y) {
}
}
// 圆形笔触
public class CircleBrush implements IBrush {
@Override
public void down(Path path, float x, float y) {
}
@Override
public void move(Path path, float x, float y) {
path.addCircle(x, y, 10, Path.Direction.CCW);
}
@Override
public void up(Path path, float x, float y) {
}
}
  • 对于每一次路径的绘制,都可以有两个命令,一个是绘制命令,另一个是撤销命令,我们将其封装为一个命令接口。注意,这里结合命令模式去构思。
1
2
3
4
5
6
7
8
9
10
11
public interface IDraw {
/**
* 绘制命令
* @param canvas 画布对象
*/
void draw(Canvas canvas);
/**
* 撤销命令
*/
void undo();
}
  • 这里只有一种绘制路径方法,即一个具体命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DrawPath implements IDraw {
public Path path; // 需要绘制的路径
public Paint paint; // 绘制画笔
@Override
public void draw(Canvas canvas) {
canvas.drawPath(path, paint);
}
@Override
public void undo() {
}
}
  • 需要一个请求者角色(Invoker)来对命令做进一步封装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class DrawInvoker {
// 绘制列表
private List<DrawPath> drawList = Collections.synchronizedList(new ArrayList<DrawPath>());
// 重做列表
private List<DrawPath> redoList = Collections.synchronizedList(new ArrayList<DrawPath>());
/**
* 增加一个命令
* @param command DrawPath
*/
public void add(DrawPath command) {
redoList.clear();
drawList.add(command);
}
/**
* 撤销上一步命令
*/
public void undo() {
if (drawList.size() > 0) {
int index = drawList.size() - 1;
DrawPath undo = drawList.get(index);
drawList.remove(index);
undo.undo();
redoList.add(undo);
}
}
/**
* 重做上一步撤销的命令
*/
public void redo() {
if (redoList.size() > 0) {
int index = redoList.size() - 1;
DrawPath redoCommand = redoList.get(index);
redoList.remove(index);
drawList.add(redoCommand);
}
}
/**
* 执行命令
*/
public void execute(Canvas canvas) {
if (drawList != null) {
for (DrawPath tmp : drawList) {
tmp.draw(canvas);
}
}
}
/**
* 是否可以重做
*/
public boolean canRedo() {
return redoList.size() > 0;
}
/**
* 是否可以撤销
*/
public boolean canUndo() {
return drawList.size() > 0;
}
}
  • 需要一个具体的接收者,这里承担重任的是一个 SurfaceView 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class DrawCanvas extends SurfaceView implements SurfaceHolder.Callback {
// 标识是否可以绘制、绘制线程是否可以运行
public boolean isDrawing, isRunning;
private Bitmap mBitmap; // 绘制到的位图对象
private DrawInvoker mInvoker; // 绘制命令请求对象
private DrawThread mThread; // 绘制线程
public DrawCanvas(Context context, AttributeSet attrs) {
super(context, attrs);
mInvoker = new DrawInvoker();
mThread = new DrawThread();
getHolder().addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
isRunning = true;
mThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
isRunning = false;
while (retry) {
try {
mThread.join();
retry = false;
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 增加一条绘制路径
* @param path DrawPath
*/
public void add(DrawPath path) {
mInvoker.add(path);
}
/**
* 撤销上一步的绘制
*/
public void undo() {
isDrawing = true;
mInvoker.undo();
}
/**
* 重做上一步撤销的绘制
*/
public void redo() {
isDrawing = true;
mInvoker.redo();
}
/**
* 是否可以重做
*/
public boolean canRedo() {
return mInvoker.canRedo();
}
/**
* 是否可以撤销
*/
public boolean canUndo() {
return mInvoker.canUndo();
}
private class DrawThread extends Thread {
@Override
public void run() {
Canvas canvas = null;
while (isRunning) {
if (isDrawing) {
try {
canvas = getHolder().lockCanvas();
if (mBitmap == null) {
mBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
Canvas c = new Canvas(mBitmap);
c.drawColor(0, PorterDuff.Mode.CLEAR);
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
mInvoker.execute(c);
canvas.drawBitmap(mBitmap, 0, 0, null);
} finally {
getHolder().unlockCanvasAndPost(canvas);
}
isDrawing = false;
}
}
}
}
}
  • 最后在 Activity 中整合各个功能模块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.xxt.xtest;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import com.xxt.xtest.demo.CircleBrush;
import com.xxt.xtest.demo.DrawCanvas;
import com.xxt.xtest.demo.DrawPath;
import com.xxt.xtest.demo.IBrush;
import com.xxt.xtest.demo.NormalBrush;
public class TestActivity extends BaseActivity {
private DrawCanvas mCanvas; // 绘制画布
private DrawPath mPath; // 路径绘制命令
private Paint mPaint; // 画笔对象
private IBrush mBrush; // 笔触对象
private Button btnRedo, btnUndo; // 重做、撤销按钮
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_test);
mPaint = new Paint();
mPaint.setColor(0xFFFFFFFF);
mPaint.setStrokeWidth(3);
mBrush = new NormalBrush();
mCanvas = findViewById(R.id.draw_canvas);
mCanvas.setOnTouchListener(new DrawTouchListener());
btnRedo = findViewById(R.id.redo_btn);
btnRedo.setEnabled(false);
btnUndo = findViewById(R.id.undo_btn);
btnUndo.setEnabled(false);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.red_btn:
mPaint = new Paint();
mPaint.setStrokeWidth(3);
mPaint.setColor(0xFFFF0000);
break;
case R.id.green_btn:
mPaint = new Paint();
mPaint.setStrokeWidth(3);
mPaint.setColor(0xFF00FF00);
break;
case R.id.blue_btn:
mPaint = new Paint();
mPaint.setStrokeWidth(3);
mPaint.setColor(0xFF0000FF);
break;
case R.id.normal_brush_btn:
mBrush = new NormalBrush();
break;
case R.id.circle_brush_btn:
mBrush = new CircleBrush();
break;
case R.id.undo_btn:
mCanvas.undo();
if (!mCanvas.canUndo()) {
btnUndo.setEnabled(false);
}
btnRedo.setEnabled(true);
break;
case R.id.redo_btn:
mCanvas.redo();
if (!mCanvas.canRedo()) {
btnRedo.setEnabled(false);
}
btnUndo.setEnabled(true);
break;
}
}
private class DrawTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (MotionEvent.ACTION_DOWN == event.getAction()) {
mPath = new DrawPath();
mPath.paint = mPaint;
mPath.path = new Path();
mBrush.down(mPath.path, event.getX(), event.getY());
} else if (MotionEvent.ACTION_MOVE == event.getAction()) {
mBrush.move(mPath.path, event.getX(), event.getY());
} else if (MotionEvent.ACTION_UP == event.getAction()) {
mBrush.up(mPath.path, event.getX(), event.getY());
mCanvas.add(mPath);
mCanvas.isDrawing = true;
btnUndo.setEnabled(true);
btnRedo.setEnabled(false);
}
return true;
}
}
}

res/layout/act_test.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.xxt.xtest.demo.DrawCanvas
android:id="@+id/draw_canvas"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/red_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:textColor="#FF0000"
android:text="红色" />
<Button
android:id="@+id/green_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:textColor="#00FF00"
android:text="绿色" />
<Button
android:id="@+id/blue_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:textColor="#0000FF"
android:text="蓝色" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/normal_brush_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="普通笔刷" />
<Button
android:id="@+id/circle_brush_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="圆形笔刷" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/undo_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="撤销" />
<Button
android:id="@+id/redo_btn"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="重做" />
</LinearLayout>
</LinearLayout>

效果图如下